﻿/*********************************************
 * vim:sw=8:ts=8:si:et
 * To use the above modeline in vim you must have "set modeline" in your .vimrc
 * Author: Guido Socher 
 * Modified by Guenther Hoelzl
 * Ported to C# and modified for NETMF by hanzibal in 2011 with permission from
 * Guenther Hoelzl
 * Copyright: GPL V2
 * http://www.gnu.org/licenses/gpl.html
 *
 * Based on the enc28j60.c file from the AVRlib library by Pascal Stang.
 * For AVRlib See http://www.procyonengineering.com/
 * Used with explicit permission of Pascal Stang.
 *
 * Title: Microchip ENC28J60 Ethernet Interface Driver
 * Chip type           : ATMEGA88 with ENC28J60
 *********************************************/

using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace ENC68J60
{
    public partial class ENC68J60uIPDriver
    {
        byte Enc28j60Bank;
        ushort NextframePtr;

        SPI spi;
        OutputPort reset;
        InterruptPort irq;

        byte[] spibuf = new byte[3];

        /// <summary>
        /// Event handler for link status changes
        /// </summary>
        /// <param name="sender">The ENC28J60 chip instance from which the event originates</param>
        /// <param name="time">Time when the event occurred</param>
        /// <param name="isUp">Indicates link status</param>
        public delegate void LinkChangedEventHandler(ENC68J60uIPDriver sender, DateTime time, bool isUp);

        /// <summary>
        /// Fires when the phy goes up or down
        /// </summary>
        public event LinkChangedEventHandler OnLinkChangedEvent;

        /// <summary>
        /// Event handler for incominig ethernet frames
        /// </summary>
        /// <param name="sender">The ENC28J60 chip instance from which the event originates</param>
        /// <param name="time">Time when the event occurred</param>
        public delegate void FrameArrivedEventHandler(ENC68J60uIPDriver sender, DateTime time);

        /// <summary>
        /// Fires when one or more incoming frames have arrived from network
        /// </summary>
        public event FrameArrivedEventHandler OnFrameArrived;

        /// <summary>
        /// Creates a new ENC28J60 driver object instance, the chip will be held
        /// in reset until the Start method is called
        /// </summary>
        /// <param name="resetPin">Netduino pin to use for chip reset</param>
        /// <param name="irqPin">Netduino pin to use for interrupt request</param>
        /// <param name="csPin">Netduino pin to use for SPI chip select</param>
        /// <param name="spiModule">Netduino SPI module to use</param>
        public ENC68J60uIPDriver(Cpu.Pin resetPin, Cpu.Pin irqPin, Cpu.Pin csPin, SPI.SPI_module spiModule)
        {
            reset = new OutputPort(resetPin, false);
            Thread.Sleep(10);

            irq = new InterruptPort(irqPin, false, Port.ResistorMode.PullUp, Port.InterruptMode.InterruptEdgeLow);
            irq.OnInterrupt += new NativeEventHandler(irq_OnInterrupt);

            var cfg = new SPI.Configuration(csPin, false, 0, 0, false, true, 12000, spiModule);
            spi = new SPI(cfg);
        }

        /// <summary>
        /// Releases the chip from reset and starts up the driver, after calling
        /// this method, it normally takes a few seconds before link is actually up
        /// </summary>
        /// <param name="mac">Six byte array containing MAC address</param>
        public void Start(byte[] mac)
        {
            reset.Write(true);
            Thread.Sleep(10);
            Init(mac);
        }

        static void DBG(string msg)
        {
            Debug.Print("ENC28j60[" + System.DateTime.Now.ToString() + "] --> " + msg);
        }

        /// <summary>
        /// Get the revision id of the chip, see current silicon errata @ Microchip
        /// </summary>
        public byte RevId
        {
            get
            {
                return ReadReg(EREVID);
            }
        }

        /// <summary>
        /// Gets a boolean indicating if link is currently up
        /// </summary>
        public bool IsLinkUp
        {
            get
            {
                return (ReadPhyReg(PHSTAT2) & PHSTAT2_LSTAT) != 0;
            }
        }

        // hanzibal: added interrupts and event routing
        // TODO: Here's a potential bug. Since async events does not seem
        // to be supported by framwork, we should fifo them to be called
        // from outside this ISR - this in order not to mess up the register bank
        // of the chip
        void irq_OnInterrupt(uint data1, uint data2, System.DateTime time)
        {
            DBG("IRQ occurred");

            byte flags;
            bool evtlink = false;

            // this lock avoids interleaved register bank swithing
            lock (irq) 
            {
                // save current bank
                var pushb = CurrentBank;

                if (((flags = ReadReg(EIR)) & EIR_LINKIF) != 0)
                {
                    DBG("IRQ_LINK: " + (IsLinkUp ? "up" : "down"));
                    ReadPhyReg(PHIR);
                    // it seems async events are not supported and we
                    // can't fire syncronous events while holding bank
                    evtlink = true;
                }

                // done using register bank, so restore it
                CurrentBank = pushb;
            }

            // now we can fire events
            if (evtlink && OnLinkChangedEvent != null) OnLinkChangedEvent.Invoke(this, time, IsLinkUp);

            if ((flags & EIR_PKTIF) != 0)
            {
                DBG("IRQ_PKT: frame(s) arrived");
                if (OnFrameArrived != null) OnFrameArrived.Invoke(this, time);
            }

            // reset NETMF irq
            irq.ClearInterrupt();
        }

        byte ReadOp(byte op, byte address)
        {
            // mac, phy and mii registers returns a dummy byte
            // before acutal data byte
            int start_rd = (address & 0x80) == 0 ? 0 : 1;

            // mask out register and add opcode
            spibuf[0] = (byte)(op|(address & 0x1f));
            spi.WriteRead(spibuf, 0, 2, spibuf, 0, 2, start_rd);
            return spibuf[1];
        }

        void WriteOp(byte op, byte address, byte data)
        {
            spibuf[0] = (byte)(op|(address & 0x1f));
            spibuf[1] = data;
            spi.WriteRead(spibuf, 0, 2, null, 0, 0, 2);
        }

        void ReadBuffer(ushort len, byte[] data)
        {
            spibuf[0] = ENC28J60_READ_BUF_MEM;
            spi.WriteRead(spibuf, 0, 1, data, 0, len, 1);
            data[len] = 0;
        }

        void WriteBuffer(ushort len, byte[] data)
        {
            byte[] op = { ENC28J60_WRITE_BUF_MEM };
            spi.Write(op);
            spi.Write(data);
        }

        byte CurrentBank
        {
            get
            {
                return (byte)((ReadOp(0, ECON1) & 0x03) << 5);
            }
            set
            {
                value &= BANK_MASK;
                // this lock serves to avouid interleaved bank swithing on the chip
                // should an interrupt occur
                lock (irq)
                {
                    // only set the bank when needed
                    if (value == Enc28j60Bank) return;
                    // set the bank
                    WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1 | ECON1_BSEL0));
                    WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, (byte)(value >> 5));
                    Enc28j60Bank = value;
                    Debug.Assert(value == CurrentBank);
                }
            }
        }

        byte ReadReg(byte address)
        {
            // set the bank
            CurrentBank = address;
            // do the read
            return ReadOp(ENC28J60_READ_CTRL_REG, address);
        }

        void WriteReg(byte address, byte data)
        {
            // set the bank
            CurrentBank = address;
            // do the write
            WriteOp(ENC28J60_WRITE_CTRL_REG, address, data);
        }

        void WritePhyReg(byte address, ushort data)
        {
            // set the PHY register address
            WriteReg(MIREGADR, address);
            // set the write command
            WriteReg(MICMD, 0);
            // write the PHY data
            WriteReg(MIWRL, (byte) data);
            WriteReg(MIWRH, (byte)(data >> 8));
            // wait until the PHY write completes
            do
            {
                Thread.Sleep(1); //delayMicroseconds(15);
            }
            while ((ReadReg(MISTAT) & MISTAT_BUSY) != 0);
        }

        // added reading of phy registers /hanzibal
        private ushort ReadPhyReg(byte address)
        {
            // set the PHY register address
            WriteReg(MIREGADR, address);
            // set the read command
            WriteReg(MICMD, MICMD_MIIRD);
            // wait until the PHY write completes
            do
            {
                Thread.Sleep(1); //delayMicroseconds(15);
            }
            while ((ReadReg(MISTAT) & MISTAT_BUSY) != 0);

            // clear the read command
            WriteReg(MICMD, 0);

            // read the PHY data
            return (ushort)(ReadReg(MIRDL) | (ReadReg(MIRDH) << 8));
        }

        /// <summary>
        /// Gets or sets the PLL clock multiplier for external clock
        /// </summary>
        public byte ClockOut
        {
            get
            {
                return ReadReg(ECOCON);
            }
            set
            {
                //setup clkout: 2 is 12.5MHz:
                WriteReg(ECOCON, (byte)(value & 0x7));
            }
        }

        void Init(byte[] macaddr)
        {
            // perform system reset
            WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
            Thread.Sleep(1); // TOOD: delay(50);

            // check CLKRDY bit to see if reset is complete
            // The CLKRDY does not work. See Rev. B4 Silicon Errata point. Just wait.
            //while(!(enc28j60Read(ESTAT) & ESTAT_CLKRDY));
            // do bank 0 stuff
            // initialize receive buffer
            // 16-bit transfers, must write low byte first
            // set receive buffer start address
            NextframePtr = RXSTART_INIT;
            // Rx start
            WriteReg(ERXSTL, RXSTART_INIT & 0xFF);
            WriteReg(ERXSTH, RXSTART_INIT >> 8);
            // set receive pointer address
            WriteReg(ERXRDPTL, RXSTART_INIT & 0xFF);
            WriteReg(ERXRDPTH, RXSTART_INIT >> 8);
            // RX end
            WriteReg(ERXNDL, RXSTOP_INIT & 0xFF);
            WriteReg(ERXNDH, RXSTOP_INIT >> 8);
            // TX start
            WriteReg(ETXSTL, TXSTART_INIT & 0xFF);
            WriteReg(ETXSTH, TXSTART_INIT >> 8);
            // TX end
            WriteReg(ETXNDL, TXSTOP_INIT & 0xFF);
            WriteReg(ETXNDH, TXSTOP_INIT >> 8);


            // do bank 1 stuff, frame filter:
            // For broadcast frames we allow only ARP packtets
            // All other frames should be unicast only for our mac (MAADR)
            //
            // The pattern to match on is therefore
            // Type     ETH.DST
            // ARP      BROADCAST
            // 06 08 -- ff ff ff ff ff ff -> ip checksum for theses bytes=f7f9
            // in binary these poitions are:11 0000 0011 1111
            // This is hex 303F->EPMM0=0x3f,EPMM1=0x30
            // WriteReg(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN);
            // WriteReg(EPMM0, 0x3f);
            // WriteReg(EPMM1, 0x30);
            // WriteReg(EPMCSL, 0xf9);
            // WriteReg(EPMCSH, 0xf7);

            // Modification by Guenther Hoelzl
            // No special frame filter for ipv6EtherShield
            // hanzibal: Well, Guenther this is not just ipv6EtherShield anymore 
            // and so the filter shoul be user configurable in future.
            WriteReg(ERXFCON, ERXFCON_UCEN | ERXFCON_BCEN | ERXFCON_MCEN);

            //
            //
            // do bank 2 stuff
            // enable MAC receive
            WriteReg(MACON1, MACON1_MARXEN | MACON1_TXPAUS | MACON1_RXPAUS);
            // bring MAC out of reset
            WriteReg(MACON2, 0x00);
            // enable automatic padding to 60bytes and CRC operations
            WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0 | MACON3_TXCRCEN | MACON3_FRMLNEN);
            // set inter-frame gap (non-back-to-back)
            WriteReg(MAIPGL, 0x12);
            WriteReg(MAIPGH, 0x0C);
            // set inter-frame gap (back-to-back)
            WriteReg(MABBIPG, 0x12);
            // Set the maximum frame size which the controller will accept
            // Do not send frames longer than MAX_FRAMELEN:
            WriteReg(MAMXFLL, MAX_FRAMELEN & 0xFF);
            WriteReg(MAMXFLH, MAX_FRAMELEN >> 8);
            // do bank 3 stuff
            // write MAC address
            // NOTE: MAC address in ENC28J60 is byte-backward
            WriteReg(MAADR5, macaddr[0]);
            WriteReg(MAADR4, macaddr[1]);
            WriteReg(MAADR3, macaddr[2]);
            WriteReg(MAADR2, macaddr[3]);
            WriteReg(MAADR1, macaddr[4]);
            WriteReg(MAADR0, macaddr[5]);
            // no loopback of transmitted frames
            WritePhyReg(PHCON2, PHCON2_HDLDIS);

            // enabling link interrupts could be interesting, down
            // seem not to be firing though
            WritePhyReg(PHIE, (ushort)(ReadPhyReg(PHIE)|18));

            // switch to bank 0
            CurrentBank = ECON1;
            // enable interrutps
            WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE | EIE_PKTIE);
            // enable frame reception
            WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
        }

        /// <summary>
        /// Send an ehternet frame to the network
        /// </summary>
        /// <param name="len">Size of the frame</param>
        /// <param name="frame">Byte array containing frame</param>
        public void SendFrame(ushort len, byte[] frame)
        {
            // Set the write pointer to start of transmit buffer area
            WriteReg(EWRPTL, TXSTART_INIT & 0xFF);
            WriteReg(EWRPTH, TXSTART_INIT >> 8);
            // Set the TXND pointer to correspond to the frame size given
            WriteReg(ETXNDL, (byte)((TXSTART_INIT + len) & 0xFF));
            WriteReg(ETXNDH, (byte)((TXSTART_INIT + len) >> 8));
            // write per-frame control byte (0x00 means use macon3 settings)
            WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00);
            // copy the frame into the transmit buffer
            WriteBuffer(len, frame);
            // send the contents of the transmit buffer onto the network
            WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
            // Reset the transmit logic problem. See Rev. B4 Silicon Errata point 12.
            if ((ReadReg(EIR) & EIR_TXERIF) != 0)
            {
                WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);
            }
        }
        
        /// <summary>
        /// Fetches parts of an ethernet frame from the network receive buffer, if one is available.
        /// </summary>
        /// <param name="maxlen">The maximum acceptable length of a retrieved frame</param>
        /// <param name="frame">Array where frame data is to be stored</param>
        /// <returns>Frame length in bytes if a frame was retrieved, zero otherwise</returns>
        public ushort ReceiveFrame(ushort maxlen, byte[] frame)
        {
            ushort rxstat;
            ushort len;
            // check if a frame has been received and buffered
            //if( !(ReadReg(EIR) & EIR_PKTIF) ){
            // The above does not work. See Rev. B4 Silicon Errata point 6.
            if (ReadReg(EPKTCNT) == 0)
            {
                return (0);
            }

            // Set the read pointer to the start of the received frame
            WriteReg(ERDPTL, (byte)NextframePtr);
            WriteReg(ERDPTH, (byte)(NextframePtr >> 8));
            // read the next frame pointer
            NextframePtr = ReadOp(ENC28J60_READ_BUF_MEM, 0);
            NextframePtr |= (ushort)(ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8);
            // read the frame length (see datasheet page 43)
            len = ReadOp(ENC28J60_READ_BUF_MEM, 0);
            len |= (ushort)(ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8);
            len -= 4; //remove the CRC count
            // read the receive status (see datasheet page 43)
            rxstat = ReadOp(ENC28J60_READ_BUF_MEM, 0);
            rxstat |= (ushort)(ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8);
            // limit retrieve length
            if (len > maxlen - 1)
            {
                len = (ushort)(maxlen - 1);
            }
            // check CRC and symbol errors (see datasheet page 44, table 7-3):
            // The ERXFCON.CRCEN is set by default. Normally we should not
            // need to check this.
            if ((rxstat & 0x80) == 0)
            {
                // invalid
                len = 0;
            }
            else
            {
                // copy the frame from the receive buffer
                ReadBuffer(len, frame);
            }
            // Move the RX read pointer to the start of the next received frame
            // This frees the memory we just read out
            WriteReg(ERXRDPTL, (byte)NextframePtr);
            WriteReg(ERXRDPTH, (byte)(NextframePtr >> 8));
            // decrement the frame counter indicate we are done with this frame
            WriteOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);
            
            return len;
        }
    }
}